
local director = mob_spawning_director
director.update_interval = 1.0
director.intensity_decay = 0.93
director.max_entities = 50


--does nothing
local function do_nothing()
end




--[[
TODO:
	support functions for each event that return staticdata and
	that are called once per event

	make plug
]]









--[[
spawn_list: List of spawn events that may happen
	description: Description for this spawn event (mostly for debugging)
	name: Name of the entity to spawn
	intensity_min: Minimum intensity requirement for the event to fire
	intensity_max: Maximum intensity requirement for the event to fire
	group_min: Minimum number of entities to spawn when event fires
	group_max: Maximum number of entities to spawn when event fires
	probability: Probability of spawning per update (excluding cooldown times)
	day_start: Number of game days before this event can start happening
	spawn_time: Spawn cooldown
	spawn_location_type: ["ground"/"air"] Where entities will appear
]]

--TODO: make an api to register stuff here
director.spawn_list = {}


-- State tracking stuff

local director_data = {} --registering table for storage
director_data =
	mob_spawning_director.intramodcom.register_for_storage("director_data", director_data)

--localize this table
local spawn_timers = director_data.spawn_timers

--api function to reset director
director.reset = function() --(re)initialize values
	director_data.intensity = 0.5
	director_data.cooldown_timer = 3

	director_data.spawn_timers = {}
	spawn_timers = director_data.spawn_timers
	director_data.last_average_health = 1.0
	director_data.last_mob_count = 0
	director.unplug()

	for _,m in ipairs(director.spawn_list)
	do
		spawn_timers[m.description] = m.spawn_time / 2

		--remove all mobs
		for i, e in pairs(minetest.luaentities)
		do
			if e.name == m.name
			then
				e.object:remove()
			end
		end
	end
end

--add getter for intensity to api
director.get_intensity = function()
	return director_data.intensity
end


do
	local spawn_event_signature =
	{
		description = "string", --unique
		name = "string", --name of entity
		intensity_min = "number", --between 0 and 1
		intensity_max = "number", --between 0 and 1
		group_min = "number",
		group_max = "number",
		probability = "number", --between 0 and 1
		day_start = "number",
		spawn_time = "number",
		spawn_location_type = "string",
		get_staticdata = "function",
		on_spawn_event = "function",
		postprocess_entity = "function",
	}
	director.register_spawn_event = function(evt)
		evt.get_staticdata = evt.get_staticdata or do_nothing
		evt.on_spawn_event = evt.on_spawn_event or do_nothing
		evt.postprocess_entity = evt.postprocess_entity or do_nothing

		do
			local function get_error_msg(key, expect, result)
				return "Error resistering spawn event: Expected " ..
					expect ..
					" in field " ..
					key ..
					" got " .. result
			end
			local key, expected, result = typing.matchSignature(evt, spawn_event_signature)
			assert(not key, key and get_error_msg(key, expected, result))
		end
		local evt_count = #director.spawn_list
		for i = 1, evt_count
		do
			assert(director.spawn_list[i].description ~= evt.description,
				"There is already a registered spawn event with description " ..
				evt.description)
		end
		director.spawn_list[evt_count + 1] = evt
		spawn_timers[evt.description] = spawn_timers[evt.description] or
			evt.spawn_time / 2
	end
end








local is_plugged
--api for plugging the spawner
do
	--[[
		The spawner can be plugged with any non-nil value. In particular
		it can be plugged with ObjectRefs. This means that when the object
		is removed, the plug vanishes too.


		plugs are stored on a special implementation of a stack as a
		table and a plug_pointer which stores the index of the top value
		or 0 if the stack is empty. If the top value is nil, the pointer
		automatically is corrected downwards.

	]]
	local plugs = {}
	local plug_pointer = 0

	--adds another plug
	function director.plug_with(value)
		assert(object ~= nil, "Can't plug spawning director with nil value")
		plug_pointer = plug_pointer + 1
		plugs[plug_pointer] = object
	end

	--removes plugging object, returns success
	function director.pop_plug(object)
		for i = plug_pointer, 1, -1
		do
			if object == plugs[i]
			then
				plugs[i] = nil
				return true
			end
		end
		return false
	end

	--removes all plugs
	function director.unplug()
		plugs = {}
		plug_pointer = 0
	end


	--checks if there are plugs
	function is_plugged()
		for i = plug_pointer, 1, -1
		do
			if plugs[i]
			then
				return true
			else
				--the top plug is no longer there.
				--correct plug_pointer
				plug_pointer = plug_pointer - 1
			end
		end
		return false
	end
end
























local function find_spawn_position(spawn_location_type)
	local players = limbo_respawning.get_living_players()
	if #players == 0
	then
		return nil
	end

	--this calculates a thing but it's NOT the center
	--possible solution divide by #players
	local center = {x = 0, y = 0, z = 0}
	for _,p in ipairs(players)
	do
		center = vector.add(center, p:get_pos())
	end
	center = vector.multiply(center, #players)

	local radius = {}
	local points = {}--stores spawning points
	for _,p in ipairs(players)
	do
		--calculate a radius for each player
		--radius gets smaller the farther the player is from the center
		local r = 20 + 10/(vector.distance(p:get_pos(), center) + 1)
		radius[p:get_player_name()] = r - 0.5 --cache radius for each player
		for j = 0, 3, 1
		do
			local pos = p:get_pos()
			--generate a random angle
			local a = math.random() * 2 * math.pi
			--add an offset to pos with angle a, radius r
			pos.x = pos.x + math.cos(a) * r
			pos.z = pos.z + math.sin(a) * r
			if spawn_location_type == "ground"
			then
				-- Move pos to on ground
				pos.y = pos.y + 10 --start high in case pos is underground
				--decide wether to go up or down depending if it's
				--underground or in the air
				local d = -1
				local node = minetest.get_node_or_nil(pos)
				if node and (minetest.registered_nodes[node.name] or
						{walkable = true}).walkable
				then
					d = 1
					pos.y = pos.y + 1
				end
				--check 40 nodes up or down from node
				for i = pos.y, pos.y + 40 * d, d
				do
					--detect ground
					local top = {x=pos.x, y=i, z=pos.z}
					local bottom = {x=pos.x, y=i-1, z=pos.z}
					local node_top = minetest.get_node_or_nil(top)
					local node_bottom = minetest.get_node_or_nil(bottom)

					if node_bottom and node_top and
						(minetest.registered_nodes[node_bottom.name] or
						{walkable = true}).walkable ~=
						(minetest.registered_nodes[node_top.name] or
						{walkable = true}).walkable
					then
						table.insert(points, top)
						break
					end
				end
			elseif spawn_location_type == "air"
			then
				-- Move pos up
				pos.y = pos.y + 12 + math.random() * 12
				--check for non-walkable node at pos
				local node = minetest.get_node_or_nil(pos)
				if node and not (minetest.registered_nodes[node.name] or
					{walkable = true}).walkable
				then
					table.insert(points, pos)
				end
			end
		end--for loop
	end

	if #points == 0
	then
		return nil
	end

	local filtered = {}
	for _,p in ipairs(players)
	do
		local pos = p:get_pos()
		for _,o in ipairs(points)
		do
			if vector.distance(pos, o) >= radius[p:get_player_name()]
			then
				table.insert(filtered, o)
			end
		end
	end

	if #filtered > 0
	then
		return filtered[math.random(#filtered)]
	end
	return nil
end

local function spawn_monsters()
	--Make list of spawn events that are eligible based on spawn timer, and properties
	local filtered = {}
	for _,m in ipairs(director.spawn_list)
	do
		if spawn_timers[m.description] <= 0
			and defense_mob_api.get_day_count() >= m.day_start --TODO clean
			and math.random() < m.probability
			and director_data.intensity >= m.intensity_min
			and director_data.intensity <= m.intensity_max
			then
			table.insert(filtered, m)
		end
	end
	if #filtered == 0
	then
		return false
	end
	--choose random spawn event
	local monster = filtered[math.random(#filtered)]

	-- Determine group size
	local intr = math.max(0, math.min(1, director_data.intensity + math.random() * 2 - 1))
	local group_size = math.floor(0.5 + monster.group_max + (monster.group_min - monster.group_max) * intr)

	-- Find the spawn position
	local pos = find_spawn_position(monster.spawn_location_type)
	if not pos
	then
		--defense:log("No spawn point found for " .. monster.description .. "!") --TODO clean
		return false
	end

	-- Spawn monsters. Amount depending on group size
	--defense:log("Spawn " .. monster.description .. " (" .. group_size .. " " ..  monster.name .. ") at " .. minetest.pos_to_string(pos))
	for i = 1, group_size
	do
		minetest.after(group_size * (math.random() * 0.2), function()
			local obj = minetest.add_entity(pos, monster.name, monster.get_staticdata(pos, group_size))
			monster.postprocess_entity(obj, pos, group_size)
		end)
	end
	monster:on_spawn_event(pos. group_size)
	spawn_timers[monster.description] = monster.spawn_time
	return true
end

local function update_intensity()
	--calculate average health
	local players = minetest.get_connected_players()
	if #players == 0
	then
		return
	end
	local average_health = 0
	for _,p in ipairs(players)
	do
		average_health = average_health + p:get_hp()
	end
	average_health = average_health / #players



	local mob_count = #minetest.luaentities

	local delta =
		--average health gain weighed with -0.2
		-0.2 * (average_health - director_data.last_average_health)
		--1 / average health weighed with 4. rises with decreasing average health
		+ 4.0 * math.max(0, 1 / average_health - 0.1) --possible division by zero doesn't matter
		--increase in mob count weighed with 0.006
		+ 0.006 * (mob_count - director_data.last_mob_count)

	director_data.last_average_health = average_health
	director_data.last_mob_count = mob_count

	director_data.intensity = math.max(0, math.min(1, director_data.intensity * director.intensity_decay + delta))
end


local function is_dark()
	local tod = minetest.get_timeofday()
	return tod < 0.2 or tod > 0.8
end


local function update()
	update_intensity()


	if director_data.cooldown_timer <= 0 and not is_plugged()
	then
		if is_dark() and
				#minetest.luaentities < director.max_entities-- and not
				--defense.debug
		then
			spawn_monsters()
		end

		if director_data.intensity > 0.5
		then

			director_data.cooldown_timer = math.random(5, 5 + 80 * (director_data.intensity - 0.5))
		end
	else
		director_data.cooldown_timer = director_data.cooldown_timer - director.update_interval
	end

	for k,v in pairs(spawn_timers)
	do
		if v > 0
		then
			spawn_timers[k] = v - director.update_interval
		end
	end
end


local do_update = director_data.enabled and update or do_nothing

function director.enable()
	director_data.enabled = true
	do_update = update
end
function director.disable()
	director_data.enabled = false
	do_update = do_nothing
end
function director.get_enablation_state()
	return director_data.enabled
end





local last_update_time = 0
minetest.register_globalstep(function(dtime)
		local gt = minetest.get_gametime()
		if last_update_time + director.update_interval < gt
		then
			do_update()
			last_update_time = gt
		end
	end)




--if director_data was not loaded, initialize it by resetting
do
	--next(director_data) returns nil if director_data is empty
	local should_reset = not next(director_data)
	if should_reset
	then
		director.reset()
	end
end
